Skip to content

Conversation

@robertbastian
Copy link
Member

@robertbastian robertbastian commented Oct 14, 2025

Currently non-Gregorian calendars implement to_calendar as Date<A> -> RataDie -> Date<Iso> -> RataDie -> Date<B>. This PR cuts out the ISO conversions to make it Date<A> -> RataDie -> Date<B>.

Gregorian calendars currently use Date<A> -> Date<Iso> -> Date<B>, where both of these conversions are free because internally they are the same. This PR preserves this behaviour by adding a HAS_CHEAP_ISO_CONVERSION const to the Calendar trait, and if both A and B have this, it goes through ISO instead of RD.

AnyCalendar still uses the Date<A> -> RataDie -> Date<Iso> -> RataDie -> Date<B> path, if we wanted to optimise this we'd need HAS_CHEAP_ISO_CONVERSION to be a method instead of a const.

@robertbastian robertbastian force-pushed the convert branch 3 times, most recently from 528b4f9 to dd1948c Compare October 20, 2025 12:26
@robertbastian robertbastian marked this pull request as ready for review October 20, 2025 14:49
@sffc
Copy link
Member

sffc commented Oct 20, 2025

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes date conversions between different calendar systems by introducing a more direct conversion path via RataDie for non-Gregorian calendars, while preserving the efficient ISO-based conversion for Gregorian ones. This is achieved by adding a HAS_CHEAP_ISO_CONVERSION constant to the Calendar trait. The changes are well-structured and result in cleaner, more efficient code, especially in the Indian calendar implementation. I've found one potential issue with handling overflow in the new Indian calendar conversion logic, which I've detailed in a comment. Overall, this is a great improvement.

sffc
sffc previously approved these changes Oct 20, 2025
Copy link
Member

@sffc sffc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fairly neutral on whether this is an improvement or not. It deletes more lines of code than it adds so I'll approve based on that.

Comment on lines 221 to 228
let inner =
if A::Calendar::HAS_CHEAP_ISO_CONVERSION && A2::Calendar::HAS_CHEAP_ISO_CONVERSION {
let iso = self.calendar.as_calendar().to_iso(self.inner());
calendar.as_calendar().from_iso(iso)
} else {
let rd = self.calendar.as_calendar().to_rata_die(self.inner());
calendar.as_calendar().from_rata_die(rd)
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: I wonder if this PR would be better going through an intermediate such as

enum RataDieOrIso { RataDie(RataDie), Iso(Date<Iso>) }

Calendars would return Iso if cheap or RataDie otherwise, and constructors would have code such as

// Calendar with cheap ISO conversion
let iso_date = match RataDieOrIso {
    RataDieOrIso::RataDie(rata_die) => Iso.from_rata_die(rata_die),
    RataDieOrIso::Iso(iso_date) => iso_date,
};

// Calendar with expensive ISO conversion
let rata_die = match RataDieOrIso {
    RataDieOrIso::RataDie(rata_die) => rata_die,
    RataDieOrIso::Iso(iso_date) => Iso.to_rata_die(iso_date),
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that ISO is only cheaper if it's cheap for both calendars, and your proposal only considers the source calendar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would work (consider Hebrew as expensive ISO and ROC as cheap ISO):

  1. Hebrew to Hebrew: converts through RataDie. OK.
  2. ROC to ROC: converts through ISO. OK, all cheap.
  3. Hebrew to ROC: Hebrew returns RataDie (expensive), then ROC converts it to ISO (casioneri), then ISO to ROC (cheap).
  4. ROC to Hebrew: ROC returns ISO (cheap), then Hebrew converts the ISO to RataDie (casioneri) and RataDie to Hebrew (expensive).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I guess that works. However, accepting RataDieOrIso means the code path being taken is not statically known, so more code gets linked and optimisations might be inhibited

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I posted another thought in #7089 (comment) which I think is better than the one in this thread.

@robertbastian robertbastian requested a review from sffc October 20, 2025 17:42
@robertbastian
Copy link
Member Author

I'm fairly neutral on whether this is an improvement or not. It deletes more lines of code than it adds so I'll approve based on that.

You have absolutely no opinion on the work this avoids for conversions between non-Gregorian calendars?

@sffc
Copy link
Member

sffc commented Oct 20, 2025

I'm fairly neutral on whether this is an improvement or not. It deletes more lines of code than it adds so I'll approve based on that.

You have absolutely no opinion on the work this avoids for conversions between non-Gregorian calendars?

Well the narrower fix to that bug would be to make to_calendar unconditionally use RataDie, or to land the casioneri code so that rata die to ISO is very cheap. I haven't formed an opinion on whether the bigger refactor is worthwhile for implementing the change.

@robertbastian
Copy link
Member Author

Well the narrower fix to that bug would be to make to_calendar unconditionally use RataDie

I'm happy to do that, I thought this would be the narrower fix

Copy link
Member

@sffc sffc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with this landing but let's get a third opinion from @Manishearth on the approach

Comment on lines 59 to 61
/// Whether `from_iso`/`to_iso` is more efficient
/// than `from_rata_die`/`to_rata_die`.
const HAS_CHEAP_ISO_CONVERSION: bool;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Another design here would be

fn cheap_to_iso(&self, date: &Self::DateInner) -> Option<IsoDateInner>;

from_iso would retain the default impl as you have here. Then the Date code would be

match from_date.cheap_to_iso(inner) {
    Some(iso_date) => to_calendar.from_iso(iso_date),
    None => to_calendar.from_rata_die(from_date.to_rata_die())
}

All of the cheap_to_iso impls should be marked as #[inline] so that DCE works nicely and avoids the branch.

I think all the pairs work:

  • ROC to ROC: uses cheap_to_iso and from_iso. OK
  • Hebrew to Hebrew: uses to_rata_die and from_rata_die. OK
  • ROC to Hebrew: uses cheap_to_iso and from_iso, where from_iso internally uses the default impl to_rata_die and from_rata_die. OK
  • Hebrew to ROC: uses to_rata_die and from_rata_die. OK

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnyCalendar still uses the Date<A> -> RataDie -> Date<Iso> -> RataDie -> Date<B> path, if we wanted to optimise this we'd need HAS_CHEAP_ISO_CONVERSION to be a method instead of a const.

I think the solution here is basically this.

Manishearth
Manishearth previously approved these changes Oct 20, 2025
Copy link
Member

@Manishearth Manishearth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally not super worried about RD-ISO conversions, but sure, why not.

Copy link
Member

@sffc sffc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed the part about AnyCalendar not benefiting from this. I think we should do a method in order for that to work. We have methods on Calendar for everything else that could be const specifically because we want AnyCalendar to work.

}

const HAS_CHEAP_ISO_CONVERSION: bool = false;
fn has_cheap_iso_conversion(&self) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: mark all the methods as #[inline]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it shouldn't be necessary for in-crate usages

@robertbastian robertbastian merged commit bfc038a into unicode-org:main Oct 21, 2025
30 checks passed
@robertbastian robertbastian deleted the convert branch October 21, 2025 15:13
Copy link
Member

@Manishearth Manishearth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a huge fan of adding yet another Calendar API, but it's fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants